Last updated: 2025-05-28

Checks: 6 1

Knit directory: /mnt/central_nas/projects/type1_diabetes/nathan/BlockCourse/2024_BlockCourse/T1D_analysis/

This reproducible R Markdown analysis was created with workflowr (version 1.7.1). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


The R Markdown file has unstaged changes. To know which version of the R Markdown file created these results, you’ll want to first commit it to the Git repo. If you’re still working on the analysis, you can ignore this warning. When you’re finished, you can run wflow_publish to commit the R Markdown file and build the HTML.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(12345) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version 955a62a. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    T1D_analysis/figure/
    Ignored:    processing/

Untracked files:
    Untracked:  T1D_analysis/05_CellCategories_cells_Uncompressed.html

Unstaged changes:
    Modified:   T1D_analysis/01_ImportData_cells_Compressed.html
    Modified:   T1D_analysis/02_SpilloverCompensation_cells_Compressed.html
    Modified:   T1D_analysis/03_TransformCorrect_cells_Compressed.html
    Modified:   T1D_analysis/04_QualityControl_cells_Compressed.html
    Modified:   T1D_analysis/05_CellCategories_cells_Compressed.Rmd
    Modified:   T1D_analysis/05_CellCategories_cells_Uncompressed.Rmd

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (T1D_analysis/05_CellCategories_cells_Compressed.Rmd) and HTML (T1D_analysis/05_CellCategories_cells_Compressed.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
Rmd 955a62a nathansteenbuck 2025-05-28 full update 2025 annotation
Rmd 35c060e nathansteenbuck 2025-05-27 minor updates pipeline
Rmd e21a12b nathansteenbuck 2024-11-20 edits scripts
Rmd f9ec97c nathansteenbuck 2024-11-20 adjust descriptions
Rmd 08f607f nathansteenbuck 2024-11-19 update channel decomposition + installation
Rmd 1f3aba7 nathansteenbuck 2024-11-12 T1D_analysis with uncompressed cell type annotation
Rmd d572272 nathansteenbuck 2024-11-11 T1D_analysis v1
Rmd 801814b nathansteenbuck 2024-10-23 minor edits, paths
Rmd 4c5de28 nathansteenbuck 2024-10-23 First backbone analysis

Rscript -e “rmarkdown::render(‘2024_BlockCourse/T1D_analysis/05_CellCategories_cells_Compressed.Rmd’)”

Goal

In the following scripts, cell types are attributed to all cells in the dataset in an iterative way:

This is performed by performing PhenoGraph clustering using the Rphenoannoy package.

For cell type annotation we perform unsupervised clustering, and then relate the lineage marker expression of clusters to cell types.

The resulting cell categories, which are used in downstream analyses are stored as colData(spe)$cell_category.

Here, the idea is to use the MINIMAL dataset to generate cell categories that can be used for the T1D dataset.

Settings

Load packages

suppressPackageStartupMessages(c(
  library(data.table),
  library(dplyr),
  library(SpatialExperiment),
  library(parallel),
  library(purrr),
  library(furrr)
))

Paths and settings

# Paths
if (!dir.exists(paths$folder_script)) dir.create(paths$folder_script)
plotsave_param$path <- paths$folder_script
plotsave_param_large$path <- paths$folder_script

# Misc settings
today <- gsub("-", "", Sys.Date())

Read in the data

Load the SpatialExperiment (SPE) object saved at the previous step.

fn_spe <- file.path(paths$folder_out, paste0(paths$object_type, "_", paths$panel_type, ".rds"))
spe <- readRDS(fn_spe)
print(spe)
class: SpatialExperiment 
dim: 27 139629 
metadata(1): spillover_matrix
assays(6): counts compcounts ... scaled fastMNN_case_id
rownames(27): H3 CD44_GCG ... DNA3 PPY
rowData names(10): channel metal ... shortname channel_name
colnames(139629): 6238_Compressed_001_1 6238_Compressed_001_2 ...
  6396_Compressed_030_1445 6396_Compressed_030_1446
colData names(30): case_id panel ... HbA1c C_peptide
reducedDimNames(0):
mainExpName: NULL
altExpNames(0):
spatialCoords names(2) : cell_x cell_y
imgData names(1): sample_id

Clustering

Settings

Load packages

RPhenoannoy can installed with: devtools::install_github("stuchly/Rphenoannoy@8b81e2e7fb0599f45070e2cba1b28ac219b7c472")

suppressPackageStartupMessages(c(
  library(ggplot2),
  library(scater),
  library(scuttle),
  library(scran),
  library(Rphenoannoy),
  library(patchwork),
  library(BiocParallel)
))

Channels and assays

Select channels (channels_clust), assays (assay_sel) and clustering methods (methods_sel) to use.

methods_sel <- c("Pheno1")

assay_sel <- c("scaled")
names(assay_sel) <- c("scaled")

writeLines(c("Assays:", assay_sel[assay_sel %in% assayNames(spe)], 
                        assay_sel[assay_sel %in% reducedDimNames(spe)]))
Assays:
scaled
dimred_sel <- c("UMAP")
writeLines(c("\nReduced dimensions:", dimred_sel))

Reduced dimensions:
UMAP
channels <- rownames(spe)[!(grepl("DNA|H3", rownames(spe)))]
cat(c("\nChannels:", channels[channels %in% rownames(spe)]))

Channels: CD44_GCG GLUT1 CD99 CD68 MPO SMA CD20_SST AMY CD3e_NKX6_1 CK19 PDX1 SYP CD45RO FOXP3 CD45RA CD8a_INS CA9 IAPP CD4_ProINS CD31 Ecdh PTPRN PCSK2 PPY
cat(c("\nNumber of channels:", length(channels)))

Number of channels: 24

Select channels to use for clustering

Reduced dimension plots showing marker expression that generated by the previous script can be used to select the most relevant markers for clustering.

In the first step, we just use Cell Category markers for clustering.

channels_clust <- rownames(rowData(spe)[rowData(spe)$clustering == 1, ])
cat(c("\nChannels used for unsupervised clustering:",
      channels_clust[channels_clust %in% rownames(spe)]))

Channels used for unsupervised clustering: CD44_GCG GLUT1 CD99 CD68 MPO SMA CD20_SST AMY CD3e_NKX6_1 CK19 PDX1 SYP CD45RO FOXP3 CD45RA CD8a_INS CA9 IAPP CD4_ProINS CD31 Ecdh PTPRN PCSK2 PPY
# Select Channels we will do our Category assignment on.
channels_cat <- c("SYP", "AMY", "CK19", 
                  "CD45RA", "CD45RO", "MPO", "CD68", 
                  "SMA", "CD31",
                  "CD3e_NKX6_1", "CD4_ProINS", "CD44_GCG", "CD20_SST", "CD8a_INS")
cat(c("\nChannels used for cell category assignment:",
      channels_cat[channels_cat %in% rownames(spe)]))

Channels used for cell category assignment: SYP AMY CK19 CD45RA CD45RO MPO CD68 SMA CD31 CD3e_NKX6_1 CD4_ProINS CD44_GCG CD20_SST CD8a_INS

Subset

# Extract the metadata (to avoid conflicts when merging the SCEs)
meta <- metadata(spe)
spatial_coords <- spatialCoords(spe)
colpairs <- colPairs(spe)

# Convert to SingleCellExperiment
sce <- as(spe, "SingleCellExperiment")
# sce_cat <- sce[channels_cat, ]
sce_cat <- spe[channels_cat, ]

Clustering: PhenoGraph

Unsupervised clustering is performed with the PhenoGraph algorithm. This method works by generating a nearest-neighbor (kNN) graph of phenotpyic similarities followed by Louvain community dectection.

Here, we try the Rphenoannoy implementation. RPhenoannoy implements a parallel Jaccard-coefficient, approximates the kNN and uses Louvain clustering.

clust_method <- c("Pheno1")
cur_assay <- "scaled"
# Number of nearest-neighbors
k <- 30
# Run Phenograph - PhenoGraph for exprs and scaled assay.
clust_name <- paste(clust_method, cur_assay, "cat", sep = "_")
writeLines(c("\n", clust_name))


Pheno1_scaled_cat
if (!clust_name %in% colnames(colData(sce_cat))) {
  set.seed(seed)
  # Run Rphenograph.
  cur_pheno_annoy <- Rphenoannoy::Rphenoannoy(t(assay(sce_cat, cur_assay)), k = k)

  cur_pheno <- DataFrame(cur_pheno_annoy[[2]]$membership)
  colnames(cur_pheno) <- clust_name
  rownames(cur_pheno) <- colnames(assay(sce_cat, cur_assay))

  # Add Phenograph clusters to the colData of the SCE object
  # Cluster `0` is attributed to non-subsetted cells and not islet cells
  colData(sce_cat)[, clust_name] <- cur_pheno
  remove(cur_pheno)
}
Run Rphenograph starts:
  -Input data of 139629 rows and 14 columns
  -k is set to 30
  Finding nearest neighbors...DONE ~ 35.958 s
  Compute jaccard coefficient between nearest-neighbor sets...
Presorting knn...
presorting DONE ~ 6.923 s
  Start jaccard
DONE ~ 0.155 s
  Build undirected graph from the weighted links...DONE ~ 2.844 s
  Run louvain clustering on the graph ...DONE ~ 82.491 s
Run Rphenograph DONE, totally takes 121.448s.
  Return a community class
  -Modularity value: 0.8481285 
  -Number of clusters: 27

Visualize clusters

Load packages

suppressPackageStartupMessages(c(
  library(heatmaply),
  library(htmltools)#,
  #library(cytomapper)
))

Subset SCE-CAT object.

set.seed(222)
if (!("subset" %in% names(metadata(sce_cat)))) {
  # Cells per case
  nb_cells <- 7500
  
  # Subset the SPE object (nb_cells per case)
  cell_subset <- tibble(rn = rownames(colData(sce_cat)),
                        case_id = colData(spe)$case_id) |> 
    group_by(case_id) |>
    sample_n(nb_cells) |> 
    pull(rn)
  
  # Keep the subset cell ids in SPE metadata
  metadata(spe)[["subset"]] <- sort(as.vector(cell_subset))
}

# Subset the SPE object
sce_sub_cat <- sce_cat[channels_cat, metadata(spe)[["subset"]]]

Visualize clusters on reduced dimensions

RUN UMAP on reduced category SPE.

# Save as variable and write assays to NULL. Reduces memory overhead for parallelization. 
# Add UMAPs to SPE object
dimred_name <- paste("UMAP", cur_assay, "cat", sep = "_")
print(dimred_name)
[1] "UMAP_scaled_cat"
# Run UMAP on a cell subset
if ((!dimred_name %in% reducedDimNames(sce_sub_cat)) && ("UMAP" %in% dimred_sel)) {
  # Extract Counts. 
  if (cur_assay %in% assayNames(spe)) {
    counts <- t(assay(sce_sub_cat, cur_assay))
  }
  # Run UMAP.
  umap_model <- uwot::umap(counts, ret_model = TRUE)
  # Extract Embedding.
  cur_umap <- umap_model$embedding
  colnames(cur_umap) <- c("UMAP1", "UMAP2")
  rownames(cur_umap) <- rownames(counts)
}
reducedDim(sce_sub_cat, dimred_name) <- cur_umap
cur_method <- "Pheno1"
cur_dimred <- "UMAP"
cur_assay <- "scaled"

cur_dat <- makePerCellDF(sce_sub_cat, use_dimred = TRUE) |>
  dplyr::mutate(case_id = factor(case_id, levels = meta$cases),
                donor_type = factor(donor_type, levels = meta$stages)) |>
  dplyr::arrange(case_id, donor_type)  |> 
  tibble::as_tibble()

dimred_name <- paste(cur_dimred, cur_assay, "cat", sep = "_")
clust_name <- paste(cur_method, cur_assay, "cat", sep = "_")

# Plot all clustersdat, dimred, color_by
p <- plot_dim_red(dat = cur_dat, dimred = dimred_name, color_by = clust_name,
                  sample = TRUE, size = 0.1, alpha = 1)
print(p)

fn <- paste0(paste(today, "Clusters", clust_name, cur_dimred,
                    sep = "_"), ".png")
do.call(ggsave, c(list(fn, p), plotsave_param))

Heatmap of marker expression by cluster

name_cur_assay <- "scaled"
clust_name <- paste(cur_method, cur_assay, "cat", sep = "_")
message(clust_name)
Pheno1_scaled_cat
message(name_cur_assay)
scaled
# Summarize the data

hm <- summarize_heatmap(sce_cat,
                        expr_values = name_cur_assay,
                        cluster_by = clust_name,
                        channels = channels_cat)

# Display the heatmap
fn <- paste0(paste(today, "Clusters", clust_name, "Heatmap",
                    sep = "_"), ".html")
print(fn)
[1] "20250528_Clusters_Pheno1_scaled_cat_Heatmap.html"
heatmaply::heatmaply(
    heatmaply::normalize(hm), main = clust_name, 
    file = file.path(paths$folder_script, fn))
# remove all clusters with less than 10 cells and plot again.
clust_freq <- table(colData(sce_cat)[[clust_name]])
keep_clusts <- names(clust_freq)[clust_freq >= 10]
sce_isl_sub <- sce_cat[, colData(sce_cat)[[clust_name]] %in% keep_clusts]

hm <- summarize_heatmap(sce_isl_sub,
                        expr_values = name_cur_assay,
                        cluster_by = clust_name,
                        channels = channels_cat)
# Display the reduced heatmap -> this improves the visualization.
# Otherwise heatmap is scaled by expression of outliers.
fn <- paste0(paste(today, "Clusters", clust_name, "Heatmap_reduced",
                    sep = "_"), ".html")
print(fn)
[1] "20250528_Clusters_Pheno1_scaled_cat_Heatmap_reduced.html"
heatmaply::heatmaply(
    heatmaply::normalize(hm), main = clust_name, 
    file = file.path(paths$folder_script, fn))

Put these into the following compartments: Islet, Immune, Exocrine, Stroma, Other.

Visualize clusters with cytomapper and cytoviewer

Here, we use the cytoviewer package to visualize the clusters on the images. Thereby, we can check if the clusters are biologically meaningful and if they indeed correspond to the respective cell types.

Additional information we can use is their spatial localization of cell types, i.e. which cells in the endocrine or exocrine compartment.

Select cluster(s) and assay to show

viz_clust <- c(1:29) # Select cluster(s) to visualize.
viz_method <- "Pheno1"
viz_assay <- "scaled"

Adjust function.

imgloader <- function(x, image_dir, image_names,
                     suffix_rem = "", suffix_add = "",
                     bit_depth = 16, type, ...) {
  require(cytomapper)

  image_list <- file.path(image_dir, image_names)

  # Test if the image list exist
  test_exist <- which(!file.exists(image_list))
  if (length(test_exist) > 0) {
    stop(c("The following images were not found:\n",
           paste(image_list[test_exist], collapse = "\n")))
  } else {
    # Load and scale the images
    images <- loadImages(image_list, ...)
    # images <- scaleImages(images, (2 ^ bit.depth) - 1)

    # Add image names to metadata
    mcols(images)$ImageName <- gsub(suffix_rem, "", names(images))
    mcols(images)$ImageName <- paste0(mcols(images)$ImageName, suffix_add)

    # Add channel names
    if (type == "stacks") {
      print("Loading image stacks")
      channelNames(images) <- rownames(x)
    }
    return(images)
  }
}

Load images and masks

spe$Pheno1_scaled_cat <- factor(sce_cat$Pheno1_scaled_cat, levels = 1:29)

if (!is.null(viz_clust)) {
  nb_images <- 14
  image_extension <- ".tiff"
  clust_name <- paste(viz_method, viz_assay, "cat", sep = "_")

  # Subset the SCE
  sce_viz <- spe[, colData(spe)[[clust_name]] %in% viz_clust]

  # Select random image.
  set.seed(seed)
  image_sub <- sort(sample(
    unique(sce_viz$image_fullname),
  min(length(unique(sce_viz$image_fullname)), nb_images)))

  # Folders
  folder_images <- file.path(paths$folder_in, "img", paths$panel_type)
  folder_masks <- file.path(paths$folder_in, "masks_cells",
                            paths$panel_type, "whole-cell")

  # Load images and masks
  images <- imgloader(
    x = sce_viz,
    image_dir = folder_images,
    image_names = image_sub,
    type = "stacks"
  )

  masks <- imgloader(
    x = sce_viz,
    image_dir = folder_masks,
    image_names = image_sub,
    as.is = TRUE,
    type = "masks"
  )

  sce_viz <- sce_viz[, sce_viz$image_fullname %in% image_sub]
  sce_viz$ImageName <- gsub(image_extension, "", sce_viz$image_fullname)
}
Loading required package: cytomapper
Loading required package: EBImage

Attaching package: 'EBImage'
The following object is masked from 'package:plotly':

    toRGB
The following object is masked from 'package:purrr':

    transpose
The following object is masked from 'package:data.table':

    transpose
The following object is masked from 'package:SummarizedExperiment':

    resize
The following object is masked from 'package:Biobase':

    channel
The following objects are masked from 'package:GenomicRanges':

    resize, tile
The following objects are masked from 'package:IRanges':

    resize, tile

Attaching package: 'cytomapper'
The following objects are masked from 'package:Biobase':

    channelNames, channelNames<-
[1] "Loading image stacks"

Interactive with Cytoviewer

# Use cytoviewer with images, masks and object
library(cytoviewer)
if (!is.null(viz_clust)) {
  channels_view <- channels_clust
  sub_images <- cytomapper::getChannels(images, channels_view)
  app <- cytoviewer(image = sub_images, 
                    mask = masks, 
                    object = sce_viz[channels_view, ], 
                    img_id = "ImageName", 
                    cell_id = "cell_number")

  #if (interactive()) {
  #  shiny::runApp(app)
  #}
}

Attribute cell categories

Note: this section requires manual intervention

Here, clusters obtained at the previous steps are manually merged into meaningful cell categories. Cluster numbers are attributed to the different cell categories based on the plots and heatmaps above. Consequently, this attribution has to be adapted to the clustering results.

Cell category attribution

Phenograph x scaled counts

clust_methods <- c("Pheno1")
clust_assay <- c("scaled")

if (!clust_assay %in% assay_sel) stop("The selected assay is not in the assays selected for clustering")
if (length(clust_assay) != 1) stop("Select only one assay")

clust_name <- paste(clust_methods, clust_assay, "cat", sep = "_")
print(clust_name)
[1] "Pheno1_scaled_cat"
# Define which clusters correspond to which cell types
# Islet: 16:19, 10
# Exocrine: 26, 22, 23, 11, 20, 8, 6, 21 
# Other: 2, 3, 13, 10, 1, 5
# Stroma: 13, 4, 25
# Immune: 7, 12, 15, 24, 27, 14
# 9

clust_other <- c(2, 3, 1, 5)
clust_islet <- c(16:19, 10)
clust_immune <- c(7, 12, 15, 24, 27, 14, 9)
clust_stroma <- c(13, 4, 25)
clust_exocrine <- c(26, 22, 23, 11, 20, 8, 6, 21)

all_clust <- sort(c(clust_islet, clust_immune, clust_exocrine, clust_stroma,clust_other))  

if ((!length(unique(all_clust)) ==
     length(unique(colData(sce_cat)[, clust_name]))) ||
    any(duplicated(all_clust))) {
  stop("Recheck cluster attribution")
}

# Add cell types to the SCE object
# Islet
colData(sce_cat)[colData(sce_cat)[, clust_name] %in% clust_islet,
                "CellCat"] <- "Islet"
colData(sce_cat)[colData(sce_cat)[, clust_name] %in% clust_immune,
                "CellCat"] <- "Immune"
colData(sce_cat)[colData(sce_cat)[, clust_name] %in% clust_exocrine,
                "CellCat"] <- "Exocrine"
colData(sce_cat)[colData(sce_cat)[, clust_name] %in% clust_stroma,
                "CellCat"] <- "Stroma"
colData(sce_cat)[colData(sce_cat)[, clust_name] %in% clust_other,
                "CellCat"]<- "Other"

Plot cell types on reduced dimensions

sce_sub_cat$Pheno1_scaled_cat <- sce_cat[, metadata(spe)[["subset"]]]$Pheno1_scaled_cat
sce_sub_cat$CellCat <- sce_cat[, metadata(spe)[["subset"]]]$CellCat

# Prepare the data
cur_dat <- makePerCellDF(sce_sub_cat, use_dimred = TRUE) |>
  mutate(case_id = factor(case_id, levels = metadata(sce_sub_cat)$cases),
         donor_type = factor(donor_type, levels = metadata(sce_sub_cat)$stages)) |>
  arrange(case_id, donor_type)

# Plot
cur_assay <- "scaled"
cur_dimred <- "UMAP"
cur_method <- "Pheno1"

dimred_name <- paste(cur_dimred, cur_assay, "cat", sep = "_")
clust_name <- "CellCat"

p <- plot_dim_red(cur_dat, dimred_name, clust_name,
                  sample = TRUE, size = 1, alpha = 1)
print(p)

fn <- paste0(paste(today, clust_name, cur_dimred,
                  sep = "_"), ".png")
do.call(ggsave, c(list(fn, p), plotsave_param))

Cell categories heatmap

name_cur_assay <- "scaled"

# Summarize the data
hm <- summarize_heatmap(sce_cat,
                        expr_values = name_cur_assay,
                        cluster_by = clust_name,
                        channels = channels_cat)

# Display the heatmap
fn <- paste0(paste(today, clust_name, "Heatmap",
                sep = "_"), ".html")

heatmaply(
heatmaply::normalize(hm), main = clust_name,
file = file.path(paths$folder_script, fn))

Number of cells by cell category

table(colData(sce_cat)[, "CellCat"])

Exocrine   Immune    Islet    Other   Stroma 
   48448    12653    20999    47091    10438 

Save

sce_cat <- sce_cat[, colnames(spe)]
spe$cell_category <- colData(sce_cat)$CellCat
table(spe$cell_category)

Exocrine   Immune    Islet    Other   Stroma 
   48448    12653    20999    47091    10438 
fn_spe <- file.path(paths$folder_out, paste0(paths$object_type, "_", paths$panel_type, ".rds"))
print(spe)
class: SpatialExperiment 
dim: 27 139629 
metadata(2): spillover_matrix subset
assays(6): counts compcounts ... scaled fastMNN_case_id
rownames(27): H3 CD44_GCG ... DNA3 PPY
rowData names(10): channel metal ... shortname channel_name
colnames(139629): 6238_Compressed_001_1 6238_Compressed_001_2 ...
  6396_Compressed_030_1445 6396_Compressed_030_1446
colData names(32): case_id panel ... Pheno1_scaled_cat cell_category
reducedDimNames(0):
mainExpName: NULL
altExpNames(0):
spatialCoords names(2) : cell_x cell_y
imgData names(1): sample_id
saveRDS(spe, fn_spe)

sessionInfo()
R version 4.3.1 (2023-06-16)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 20.04.6 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0 
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0

locale:
 [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
 [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
 [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
[10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   

time zone: Etc/UTC
tzcode source: system (glibc)

attached base packages:
[1] parallel  stats4    stats     graphics  grDevices utils     datasets 
[8] methods   base     

other attached packages:
 [1] cytoviewer_1.2.0            cytomapper_1.14.0          
 [3] EBImage_4.44.0              htmltools_0.5.8.1          
 [5] heatmaply_1.5.0             viridis_0.6.5              
 [7] viridisLite_0.4.2           plotly_4.10.4              
 [9] BiocParallel_1.36.0         patchwork_1.2.0            
[11] Rphenoannoy_0.1.0           Matrix_1.6-5               
[13] igraph_2.1.4                scran_1.30.2               
[15] scater_1.30.1               scuttle_1.12.0             
[17] furrr_0.3.1                 future_1.49.0              
[19] purrr_1.0.2                 data.table_1.17.2          
[21] SpatialExperiment_1.12.0    SingleCellExperiment_1.24.0
[23] SummarizedExperiment_1.32.0 Biobase_2.62.0             
[25] GenomicRanges_1.54.1        GenomeInfoDb_1.38.8        
[27] IRanges_2.36.0              S4Vectors_0.40.2           
[29] BiocGenerics_0.48.1         MatrixGenerics_1.14.0      
[31] matrixStats_1.5.0           dplyr_1.1.4                
[33] ggplot2_3.5.1              

loaded via a namespace (and not attached):
  [1] RcppAnnoy_0.0.22          later_1.3.2              
  [3] bitops_1.0-9              svgPanZoom_0.3.4         
  [5] tibble_3.2.1              lifecycle_1.0.4          
  [7] edgeR_4.0.16              rprojroot_2.0.4          
  [9] globals_0.18.0            lattice_0.22-7           
 [11] crosstalk_1.2.1           dendextend_1.17.1        
 [13] magrittr_2.0.3            limma_3.58.1             
 [15] sass_0.4.9                rmarkdown_2.27           
 [17] jquerylib_0.1.4           yaml_2.3.10              
 [19] metapod_1.10.1            httpuv_1.6.15            
 [21] sp_2.1-4                  RColorBrewer_1.1-3       
 [23] abind_1.4-8               zlibbioc_1.48.2          
 [25] RCurl_1.98-1.14           git2r_0.33.0             
 [27] seriation_1.5.5           GenomeInfoDbData_1.2.11  
 [29] ggrepel_0.9.5             irlba_2.3.5.1            
 [31] listenv_0.9.1             terra_1.7-78             
 [33] dqrng_0.4.1               parallelly_1.44.0        
 [35] svglite_2.1.3             DelayedMatrixStats_1.24.0
 [37] codetools_0.2-20          DelayedArray_0.28.0      
 [39] tidyselect_1.2.1          raster_3.6-26            
 [41] farver_2.1.2              ScaledMatrix_1.10.0      
 [43] TSP_1.2-4                 webshot_0.5.5            
 [45] jsonlite_2.0.0            BiocNeighbors_1.20.2     
 [47] iterators_1.0.14          systemfonts_1.1.0        
 [49] foreach_1.5.2             tools_4.3.1              
 [51] ragg_1.3.2                Rcpp_1.0.14              
 [53] glue_1.8.0                gridExtra_2.3            
 [55] SparseArray_1.2.4         xfun_0.52                
 [57] ca_0.71.1                 HDF5Array_1.30.1         
 [59] shinydashboard_0.7.2      withr_3.0.2              
 [61] fastmap_1.2.0             rhdf5filters_1.14.1      
 [63] bluster_1.12.0            fansi_1.0.6              
 [65] digest_0.6.37             rsvd_1.0.5               
 [67] R6_2.6.1                  mime_0.13                
 [69] textshaping_0.4.0         colorspace_2.1-1         
 [71] jpeg_0.1-11               utf8_1.2.5               
 [73] tidyr_1.3.1               generics_0.1.4           
 [75] httr_1.4.7                htmlwidgets_1.6.4        
 [77] S4Arrays_1.2.1            whisker_0.4.1            
 [79] uwot_0.2.2                pkgconfig_2.0.3          
 [81] gtable_0.3.6              registry_0.5-1           
 [83] workflowr_1.7.1           XVector_0.42.0           
 [85] fftwtools_0.9-11          scales_1.3.0             
 [87] png_0.1-8                 knitr_1.47               
 [89] reshape2_1.4.4            rjson_0.2.23             
 [91] rhdf5_2.46.1              cachem_1.1.0             
 [93] stringr_1.5.1             shinycssloaders_1.0.0    
 [95] miniUI_0.1.1.1            vipor_0.4.7              
 [97] pillar_1.9.0              grid_4.3.1               
 [99] vctrs_0.6.5               RANN_2.6.2               
[101] promises_1.3.0            BiocSingular_1.18.0      
[103] beachmat_2.18.1           xtable_1.8-4             
[105] cluster_2.1.8.1           archive_1.1.12           
[107] beeswarm_0.4.0            evaluate_1.0.3           
[109] magick_2.8.3              cli_3.6.5                
[111] locfit_1.5-9.9            compiler_4.3.1           
[113] rlang_1.1.6               crayon_1.5.3             
[115] labeling_0.4.3            plyr_1.8.9               
[117] fs_1.6.6                  ggbeeswarm_0.7.2         
[119] stringi_1.8.7             nnls_1.6                 
[121] assertthat_0.2.1          munsell_0.5.1            
[123] lazyeval_0.2.2            tiff_0.1-12              
[125] colourpicker_1.3.0        sparseMatrixStats_1.14.0 
[127] Rhdf5lib_1.24.2           statmod_1.5.0            
[129] shiny_1.8.1.1             highr_0.11               
[131] fontawesome_0.5.3         memoise_2.0.1            
[133] bslib_0.7.0